using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Lucene.Linq.Mapping;
using Lucene.Linq.Utility;

namespace Lucene.Linq.Expressions
{
    /// <summary>
    /// First-pass expression tree visitor. 
    /// Binds and replaces tree some of the expression tree with custom expressions
    /// </summary>
    internal class QueryBinder : ExpressionVisitor
    {
        private readonly FieldProjector _fieldProjector;
        private readonly AnalyzerTypeCache _typeCache;
        private int _aliasCount;
        private Dictionary<ParameterExpression, Expression> _map;

        #region Ctors

        internal QueryBinder()
        {
            _fieldProjector = new FieldProjector(CanBeField);
            _typeCache = AnalyzerTypeCache.Instance;
        }

        #endregion

        private bool CanBeField(Expression expression)
        {
            return expression.NodeType == (ExpressionType)LuceneExpressionType.Field;
        }

        internal Expression Bind(Expression expression)
        {
            _map = new Dictionary<ParameterExpression, Expression>();
            return Visit(expression);
        }


        private string GetNextAlias()
        {
            return "t" + (_aliasCount++);
        }

        private ProjectedFields ProjectFields(Expression expression, string newAlias, string existingAlias)
        {
            return _fieldProjector.ProjectFields(expression, newAlias, existingAlias);
        }

        protected override Expression VisitMethodCall(MethodCallExpression m)
        {
            if (m.Method.DeclaringType == typeof(Queryable) || m.Method.DeclaringType == typeof(Enumerable))
            {
                switch (m.Method.Name)
                {
                    case "Where":
                        return BindWhere(m.Type, m.Arguments[0], (LambdaExpression)(m.Arguments[1].StripQuotes()));
                    case "Select":
                        return BindSelect(m.Type, m.Arguments[0], (LambdaExpression)(m.Arguments[1]).StripQuotes());
                    case "Group":
                        throw new NotSupportedException("Grouping is not supported with LINQ to Lucene at this stage");

                    case "Count":
                        return BindCount(m.Arguments[0]);
                    case "Skip":
                        return BindSkip(m.Arguments[0], m.Arguments[1]);
                    case "Take":
                        return BindTake(m.Arguments[0], m.Arguments[1]);
                    case "ElementAt":
                        return BindElementAt(m.Arguments[0], m.Arguments[1]);
                    case "First":
                        return BindElementAt(m.Arguments[0], Expression.Constant(0));
                    case "Last":
                        return BindElementAt(m.Arguments[0], Expression.Constant(0));

                    case "OrderBy":
                        return BindOrderBy(m.Arguments[0], (LambdaExpression)(m.Arguments[1]).StripQuotes(), false);
                    case "OrderByDescending":
                        return BindOrderBy(m.Arguments[0], (LambdaExpression)(m.Arguments[1]).StripQuotes(), true);

                    case "ThenBy":
                        return BindThenOrderBy(m.Arguments[0], (LambdaExpression)(m.Arguments[1]).StripQuotes(), false);
                    case "ThenByDescending":
                        return BindThenOrderBy(m.Arguments[0], (LambdaExpression)(m.Arguments[1]).StripQuotes(), true);
                }
                throw new NotSupportedException(string.Format("The method '{0}' is not supported", m.Method.Name));
            }

            return base.VisitMethodCall(m);
        }

        private Expression BindThenOrderBy(Expression source, LambdaExpression orderByLambda, bool isDescending)
        {
            // source is queryable
            var projection = (ProjectionExpression)Visit(source);

            // extract the MemberAccess from the orderby lambda expression
            if (orderByLambda.Body is MemberExpression)
            {
                var memberExpression = orderByLambda.Body as MemberExpression;
                if ((memberExpression.Member is PropertyInfo) == false)
                {
                    throw new ArgumentException("Order by lambda expression should reference property");
                }

                return new OrderThenByExpression(projection, memberExpression.Member as PropertyInfo, isDescending);
            }
            throw new ArgumentException("Invalid order by expression. Body of expression should access a member");
        }

        private Expression BindOrderBy(Expression source, LambdaExpression orderByLambda, bool isDescending)
        {
            // source is queryable
            var projection = (ProjectionExpression)Visit(source);

            // extract the MemberAccess from the orderby lambda expression
            if (orderByLambda.Body is MemberExpression)
            {
                var memberExpression = orderByLambda.Body as MemberExpression;
                if ((memberExpression.Member is PropertyInfo) == false)
                {
                    throw new ArgumentException("Order by lambda expression should reference property");
                }

                return new OrderByExpression(projection, memberExpression.Member as PropertyInfo, isDescending);
            }
            throw new ArgumentException("Invalid order by expression. Body of expression should access a member");
        }

        private Expression BindElementAt(Expression source, Expression elementAtExpression)
        {
            var projection = (ProjectionExpression)Visit(source);
            return new ElementAtExpression(projection, Visit(elementAtExpression));
        }

        private Expression BindTake(Expression source, Expression takeCount)
        {
            var projection = (ProjectionExpression)Visit(source);

            //REVIEW: not sure why i put this here with my WalkMethod stuff
            Expression skipExp = Expression.Constant(0, typeof(int));

            if (projection is SkipExpression)
            {
                skipExp = (projection as SkipExpression).SkipAmount;
            }

            // create an expression to convert to skip and take, wrapping the projector
            return new TakeExpression(projection, skipExp, takeCount);
        }

        private Expression BindSkip(Expression source, Expression count)
        {
            var projection = (ProjectionExpression)Visit(source);
            return new SkipExpression(projection, Visit(count));
        }

        private Expression BindCount(Expression source)
        {
            var projection = (ProjectionExpression)Visit(source);
            return new CountExpression(projection);
        }

        private Expression BindWhere(Type resultType, Expression source, LambdaExpression predicate)
        {
            var projection = (ProjectionExpression)Visit(source);
            _map[predicate.Parameters[0]] = projection.Projector;
            Expression where = Visit(predicate.Body);
            string alias = GetNextAlias();
            ProjectedFields pf = ProjectFields(projection.Projector, alias, GetExistingAlias(projection.Source));

            return new ProjectionExpression(
                new SelectExpression(resultType, alias, pf.Fields, null, projection.Source, where),
                pf.Projector
                );
        }

        private Expression BindSelect(Type resultType, Expression source, LambdaExpression selector)
        {
            var projection = (ProjectionExpression)Visit(source);
            _map[selector.Parameters[0]] = projection.Projector;
            Expression expression = Visit(selector.Body);
            string alias = GetNextAlias();
            ProjectedFields pf = ProjectFields(expression, alias, GetExistingAlias(projection.Source));

            return new ProjectionExpression(
                new SelectExpression(resultType, alias, pf.Fields, null, projection.Source, null),
                pf.Projector
                );
        }

        private static string GetExistingAlias(Expression source)
        {
            switch ((LuceneExpressionType)source.NodeType)
            {
                case LuceneExpressionType.Select:
                    return ((SelectExpression)source).Alias;
                case LuceneExpressionType.Index:
                    return ((IndexExpression)source).Alias;
                default:
                    throw new InvalidOperationException(string.Format("Invalid source node type '{0}'", source.NodeType));
            }
        }

        private bool IsIndex(object value)
        {
            var q = value as IQueryable;
            return q != null && q.Expression.NodeType == ExpressionType.Constant;
        }

        private string GetIndexName(object index)
        {
            var indexQuery = (IQueryable)index;
            return indexQuery.ElementType.ReadDocumentDetails().Name;
        }

        private static IEnumerable<string> GetDefaultFieldNames(Type documentType)
        {
            return (from fd in documentType.ReadFieldDetails()
                    where fd.Field.IsDefault
                    select fd.Name).ToArray();
        }

        private ProjectionExpression GetIndexProjection(object value)
        {
            var index = (IQueryable)value;
            string indexAlias = GetNextAlias();
            string selectAlias = GetNextAlias();
            var bindings = new List<MemberBinding>();
            var fields = new List<FieldDeclaration>();
            string[] defaultFieldNames = GetDefaultFieldNames(index.ElementType).ToArray();

            foreach (FieldDetails fd in index.ElementType.ReadFieldDetails())
            {
                string fieldName = fd.Name;
                Type fieldType = fd.Property.PropertyType;
                int ordinal = fields.Count;

                bindings.Add(Expression.Bind(fd.Property,
                                             new FieldExpression(fieldType, selectAlias, fieldName, ordinal,
                                                                 _typeCache[fd.Field.Analyzer])));
                fields.Add(new FieldDeclaration(fieldName,
                                                new FieldExpression(fieldType, indexAlias, fieldName, ordinal,
                                                                    _typeCache[fd.Field.Analyzer])));
            }

            Expression projector = Expression.MemberInit(Expression.New(index.ElementType), bindings);
            Type resultType = typeof(IEnumerable<>).MakeGenericType(index.ElementType);

            return new ProjectionExpression(
                new SelectExpression(
                    resultType,
                    selectAlias,
                    fields,
                    defaultFieldNames,
                    new IndexExpression(resultType, indexAlias, GetIndexName(index)),
                    null
                    ),
                projector
                );
        }

        protected override Expression VisitConstant(ConstantExpression c)
        {
            if (IsIndex(c.Value))
            {
                return GetIndexProjection(c.Value);
            }

            return c;
        }

        protected override Expression VisitParameter(ParameterExpression p)
        {
            Expression e;
            if (_map.TryGetValue(p, out e))
            {
                return e;
            }

            return p;
        }

        protected override Expression VisitMemberAccess(MemberExpression m)
        {
            Expression source = Visit(m.Expression);
            switch (source.NodeType)
            {
                case ExpressionType.MemberInit:
                    var min = (MemberInitExpression)source;
                    for (int i = 0, n = min.Bindings.Count; i < n; i++)
                    {
                        var assign = min.Bindings[i] as MemberAssignment;
                        if (assign != null && MembersMatch(assign.Member, m.Member))
                        {
                            return assign.Expression;
                        }
                    }
                    break;

                case ExpressionType.New:
                    var nex = (NewExpression)source;
                    for (int i = 0, n = nex.Members.Count; i < n; i++)
                    {
                        if (MembersMatch(nex.Members[i], m.Member))
                        {
                            return nex.Arguments[i];
                        }
                    }
                    break;
            }

            return source == m.Expression ? m : MakeMemberAccess(source, m.Member);
        }

        private static bool MembersMatch(MemberInfo a, MemberInfo b)
        {
            return a == b || (a is MethodInfo && b is PropertyInfo
                                  ? a == ((PropertyInfo)b).GetGetMethod()
                                  : a is PropertyInfo && b is MethodInfo && ((PropertyInfo)a).GetGetMethod() == b);
        }

        private Expression MakeMemberAccess(Expression source, MemberInfo mi)
        {
            var fi = mi as FieldInfo;
            if (fi != null)
            {
                return Expression.Field(source, fi);
            }

            var pi = (PropertyInfo)mi;
            return Expression.Property(source, pi);
        }
    }
}